/* dircproxy
 * Copyright (C) 2002 Scott James Remnant <scott@netsplit.com>.
 * All Rights Reserved.
 *
 * dns.c
 *  - non-blocking DNS lookups using callbacks
 *  - wrappers around /etc/services lookup functions
 *
 * The non-blocking stuff is a little complex, but it means that the main
 * loop can continue while waiting for DNS requests to complete.  Completion
 * of a DNS request is notified by the child death signal, so it will
 * interrupt the main loop to continue where you left off.
 * --
 * @(#) $Id: dns.c,v 1.14 2001/12/21 20:15:55 keybuk Exp $
 *
 * This file is distributed according to the GNU General Public
 * License.  For full details, read the top of 'main.c' or the
 * file called COPYING that was distributed with this code.
 */

#include <sys/param.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <netdb.h>

#include <dircproxy.h>
#include "sprintf.h"
#include "dns.h"

/* Structure used to hold information about a dns child */
struct dnschild {
  pid_t pid;
  int pipe;

  struct in_addr addr;

  void (*function)(void *, void *, struct in_addr *, const char *);
  void *boundto;
  void *data;

  struct dnschild *next;
};

/* Reply generated by a child */
struct dnsresult {
  int success;
  struct in_addr addr;
  char name[256];
};

/* forward declarations */
static struct dnsresult _dns_lookup(const char *, struct in_addr *);
static int _dns_startrequest(void *, void (*)(void *, void *,
                                              struct in_addr *, const char *),
                             void *, struct in_addr *, const char *);

/* Children */
static struct dnschild *dnschildren = 0;

/* Function that looks up DNS request */
static struct dnsresult _dns_lookup(const char *name, struct in_addr *addr) {
  struct dnsresult res;
  struct hostent *info;
  pid_t pid;

  pid = getpid();

  memset(&res, 0, sizeof(struct dnsresult));
  if (name) {
    debug("%d: Looking up IP for '%s'", pid, name);
    info = gethostbyname(name);
  } else if (addr) {
    debug("%d: Lookup up name for '%s'", pid, inet_ntoa(*addr));
    info = gethostbyaddr((char *)addr, sizeof(struct in_addr), AF_INET);
  } else {
    info = 0;
  }

  if (info) {
    res.success = 1;

    res.addr.s_addr = *((unsigned long *) info->h_addr);
    if (info->h_name) {
      strncpy(res.name, info->h_name, 255);
      res.name[255] = 0;
    }

    debug("%d: Got '%s' (%s)", pid, res.name, inet_ntoa(res.addr));
  } else {
    debug("%d: No result", pid);
  }

  return res;
}

/* Function that starts a non-blocking DNS request. */
static int _dns_startrequest(void *boundto,
                             void (*function)(void *, void *,
                                              struct in_addr *, const char *),
                             void *data, struct in_addr *addr, const char *name)
{
  struct dnschild *child;
  int p[2], wp[2];

  /* Pipe where the result will be placed on success */
  if (pipe(p)) {
    syscall_fail("pipe", "p", 0);
    function(boundto, data, 0, 0);
    return -1;
  }

  /* Pipe to indicate the child can go */
  if (pipe(wp)) {
    close(p[0]);
    close(p[1]);
    syscall_fail("pipe", "wp", 0);
    function(boundto, data, 0, 0);
    return -1;
  }

  /* Allocate and place the child on the list now, to avoid race conditions */
  child = (struct dnschild *)malloc(sizeof(struct dnschild));
  child->pipe = p[0];
  child->function = function;
  child->boundto = boundto;
  child->addr.s_addr = (addr ? addr->s_addr : 0);
  child->data = data;
  child->next = dnschildren;
  dnschildren = child;

  /* Fork */
  child->pid = fork();
  if (child->pid == -1) {
    /* Error */
    syscall_fail("fork", 0, 0);
    dnschildren = child->next;
    close(p[1]);
    close(p[0]);
    close(wp[1]);
    close(wp[0]);
    free(child);
    function(boundto, data, 0, 0);
    return -1;
    
  } else if (child->pid) {
    /* Parent */
    debug("New DNS process started, pid %d", child->pid);
    close(p[1]);
    close(wp[0]);

    /* Send go signal */
    write(wp[1], "go", 2);
    close(wp[1]);
    return 0;

  } else {
    struct dnsresult result;
    char gobuf[2];

    close(p[0]);
    close(wp[1]);
    
    /* Use ALARM to do timeouts */
    signal(SIGALRM, SIG_DFL);
    alarm(g.dns_timeout);

    /* Wait for a go signal */
    read(wp[0], gobuf, 2);
    close(wp[0]);
    
    /* Do the lookup */
    result = _dns_lookup(name, addr);
    if (result.success) {
      /* Succeded, write to our parent and die */
      write(p[1], (void *)&result, sizeof(struct dnsresult));
      exit(0);
    } else {
      /* Didn't succeed */
      exit(1);
    }
  }
}

/* Called to end a DNS request.  0 = Not handled, >0 = Ok, <0 = Error */
int dns_endrequest(pid_t pid, int status) {
  struct dnschild *lastchild, *child;
  struct dnsresult result;
  struct in_addr *addr;
  char *name;
  size_t len;

  /* Check to see whether this was a DNS child */
  child = dnschildren;
  lastchild = 0;
  while (child) {
    if (child->pid == pid)
      break;

    lastchild = child;
    child = child->next;
  }
  if (!child)
    return 0;

  /* Remove it from the list */
  if (lastchild) {
    lastchild->next = child->next;
  } else {
    dnschildren = child->next;
  }

  debug("%d: Was a DNS child, getting result", pid);

  /* Parameters to call function with */
  name = 0;
  addr = 0;
  
  /* Read from pipe if child returned normally */
  if (WIFEXITED(status)) {
    if (!WEXITSTATUS(status)) {
      len = read(child->pipe, (void *)&result, sizeof(struct dnsresult));
      if (len != sizeof(struct dnsresult)) {
        syscall_fail("read", 0, 0);
      } else if (result.success) {
        debug("%d: Got result", pid);

        addr = &(result.addr);
        name = result.name;
      } else {
        debug("%d: DNS lookup failed", pid);
      }
    } else {
      debug("%d: DNS lookup returned %d", pid, WEXITSTATUS(status));
    }
  } else if (WIFSIGNALED(status)) {
    debug("%d: DNS lookup terminated with signal %d", pid, WTERMSIG(status));
  } else {
    debug("%d: DNS lookyp terminated abnormally", pid);
  }

  /* If DNS failed, but we were looking up an IP address, fill that */
  if (!addr && child->addr.s_addr) {
    result.addr.s_addr = child->addr.s_addr;
    addr = &(result.addr);
  }

  /* If DNS failed but we have an IP, fill the name in with inet_ntoa() */
  if (addr && (!name || !strlen(name))) {
    char *ip;

    ip = inet_ntoa(*addr);
    strncpy(result.name, ip, 255);
    result.name[255] = 0;

    debug("%d: Changed name to '%s'", pid, result.name);
    name = result.name;
  }

  /* Call the function */
  child->function(child->boundto, child->data, addr, name);

  /* Clean up */
  close(child->pipe);
  free(child);

  return 1;
}

/* Kill off any children associated with an ircproxy */
int dns_delall(void *b) {
  struct dnschild *c, *l;
  int numdone;

  l = 0;
  c = dnschildren;
  numdone = 0;
  
  while (c) {
    if (c->boundto == b) {
      struct dnschild *n;

      n = c->next;
      debug("Killing DNS process %d", c->pid);
      kill(c->pid, SIGKILL);
      close(c->pipe);
      free(c);

      if (l) {
        c = l->next = n;
      } else {
        c = dnschildren = n;
      }
    } else {
      l = c;
      c = c->next;
    }
  }

  return numdone;
}

/* Kill off ALL dns children */
void dns_flush(void) {
  struct dnschild *c;

  c = dnschildren;
  while (c) {
    struct dnschild *n;

    n = c->next;
    debug("Killing DNS process %d", c->pid);
    kill(c->pid, SIGKILL);
    close(c->pipe);
    free(c);
    c = n;
  }

  dnschildren = 0;
}

/* Returns the IP address of a hostname */
int dns_addrfromhost(void *boundto, void *data, const char *name,
                     void (*function)(void *, void *,
                                      struct in_addr *, const char *)) {
  return _dns_startrequest(boundto, function, data, 0, name);
}

/* Returns the hostname of an IP address */
int dns_hostfromaddr(void *boundto, void *data, struct in_addr addr,
                     void (*function)(void *, void *,
                                      struct in_addr *, const char *)) {
  return _dns_startrequest(boundto, function, data, &addr, 0);
}

/* Fill a sockaddr_in from a hostname or hostname:port combo thing */
int dns_filladdr(void *boundto, const char *name,
                 const char *defaultport, int allowcolon,
                 struct sockaddr_in *result,
                 void (*function)(void *, void *,
                                  struct in_addr *, const char *),
                 void *data) {
  char *addr, *port;
  int ret = 0;

  memset(result, 0, sizeof(struct sockaddr_in));
  result->sin_family = AF_INET;
  if (defaultport)
    result->sin_port = dns_portfromserv(defaultport);

  addr = x_strdup(name);

  if (allowcolon) {
    port = strchr(addr, ':');

    if (port) {
      *(port++) = 0;

      if (strlen(port))
        result->sin_port = dns_portfromserv(port);
    }
  }

  ret = _dns_startrequest(boundto, function, data, 0, addr);

  free(addr);
  return ret;
}

/* Returns a network port number for a port as a string */
int dns_portfromserv(const char *serv) {
  struct servent *entry;

  entry = getservbyname(serv, "tcp");
  return (entry ? entry->s_port : htons(atoi(serv)));
}

/* Returns a service name for a network port number */
char *dns_servfromport(int port) {
  struct servent *entry;
  char *str;

  entry = getservbyport(port, "tcp");
  if (entry) {
    str = x_strdup(entry->s_name);
  } else {
    str = x_sprintf("%d", port);
  }

  return str;
}
